<!DOCTYPE html>
<html>
    <head>
        <script src="/w3c/resources/testharness.js"></script>
        <script src="/w3c/resources/testharnessreport.js"></script>
        <script src="mediasource-util.js"></script>
        <link rel='stylesheet' href='/w3c/resources/testharness.css'>
    </head>
    <body>
        <div id="log"></div>
        <script>
          function mediasource_duration_below_currentTime_seek_test(testFunction, description, options)
          {
              return mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
              {
                  assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration');

                  var fullDuration = segmentInfo.duration;
                  var seekTo = fullDuration / 2.0;
                  var reducedDuration = seekTo / 2.0;

                  var receivedTimeupdate = false;
                  var timeupdateEventHandler = test.step_func(function(event) { receivedTimeupdate = true; });

                  mediaElement.play();

                  // Append all the segments
                  test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer append completed');
                  test.expectEvent(mediaElement, 'playing', 'Playing triggered');
                  sourceBuffer.appendBuffer(mediaData);


                  test.waitForExpectedEvents(function()
                  {
                      assert_equals(mediaElement.duration, fullDuration, 'mediaElement duration matches fullDuration');
                      assert_equals(mediaSource.duration, fullDuration, 'mediaSource duration matches fullDuration');

                      test.expectEvent(mediaElement, 'seeking', 'seeking to seekTo');
                      // Intentionally not expecting exactly 1 timeupdate here. currentTime is moving, so extra events could occur.
                      mediaElement.addEventListener('timeupdate', timeupdateEventHandler, { once: true });
                      test.expectEvent(mediaElement, 'seeked', 'seeked to seekTo');

                      mediaElement.currentTime = seekTo;

                      assert_true(mediaElement.seeking, 'mediaElement.seeking (to seekTo)');
                  });

                  test.waitForExpectedEvents(function()
                  {
                      assert_true(receivedTimeupdate, 'mediaElement timeupdate occurred at least once since playing and through starting, doing and completing seek');
                      assert_greater_than_equal(mediaElement.currentTime, seekTo, 'Playback time has reached seekTo');
                      assert_equals(mediaElement.duration, fullDuration, 'mediaElement duration matches fullDuration after seekTo');
                      assert_equals(mediaSource.duration, fullDuration, 'mediaSource duration matches fullDuration after seekTo');
                      assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to seekTo');

                      // Explicitly remove buffered media beyond the new reduced duration prior to reducing duration.
                      // Implicit removal of buffered media as part of duration reduction is disallowed as of
                      // https://github.com/w3c/media-source/pull/65/
                      test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer range removal completed');
                      sourceBuffer.remove(reducedDuration, fullDuration);
                      assert_true(sourceBuffer.updating, 'sourceBuffer.updating during range removal');
                  });

                  test.waitForExpectedEvents(function()
                  {
                      assert_false(sourceBuffer.updating, 'sourceBuffer.updating after range removal');
                      assert_greater_than_equal(mediaElement.currentTime, seekTo,
                                                'Playback time is still at least seekTo after range removal');

                      test.expectEvent(mediaElement, 'seeking', 'Seeking to reduced duration');
                      mediaSource.duration = reducedDuration;
                      assert_true(mediaElement.seeking, 'Seeking after setting reducedDuration');
                  });

                  test.waitForExpectedEvents(function()
                  {
                      assert_equals(mediaElement.currentTime, reducedDuration,
                                    'Playback time is reducedDuration while seeking');
                      assert_true(mediaElement.seeking, 'mediaElement.seeking while seeking to reducedDuration');
                      assert_equals(mediaElement.duration, reducedDuration,
                                    'mediaElement duration matches reducedDuration during seek to it');
                      assert_equals(mediaSource.duration, reducedDuration,
                                    'mediaSource duration matches reducedDuration during seek to it');

                      // FIXME: Confirm 'waiting' and then 'stalled' fire here. See http://crbug.com/266592.

                      testFunction(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData,
                                   reducedDuration);
                  });
              }, description, options);
          }

          mediasource_duration_below_currentTime_seek_test(function(test, mediaElement, mediaSource, segmentInfo,
                                                           sourceBuffer, mediaData, reducedDuration)
          {
              // Tests that duration reduction below current playback position
              // starts seek to new duration.
              test.done();
          }, 'Test seek starts on duration reduction below currentTime');

          mediasource_duration_below_currentTime_seek_test(function(test, mediaElement, mediaSource, segmentInfo,
                                                           sourceBuffer, mediaData, reducedDuration)
          {
              // The duration has been reduced at this point, and there is an
              // outstanding seek pending.
              test.expectEvent(sourceBuffer, 'updateend', 'updateend after appending more data');

              // FIXME: Confirm 'playing' fires here. See http://crbug.com/266592.

              test.expectEvent(mediaElement, 'timeupdate', 'timeupdate while finishing seek to reducedDuration');
              test.expectEvent(mediaElement, 'seeked', 'seeked to reducedDuration');

              // Allow seek to complete by appending more data beginning at the
              // reduced duration timestamp.
              sourceBuffer.timestampOffset = reducedDuration;
              sourceBuffer.appendBuffer(mediaData);

              test.waitForExpectedEvents(function()
              {
                  assert_greater_than_equal(mediaElement.currentTime, reducedDuration,
                                            'Playback time has reached reducedDuration');
                  assert_approx_equals(mediaElement.duration, reducedDuration + segmentInfo.duration, 0.05,
                                       'mediaElement duration increased by new append');
                  assert_equals(mediaSource.duration, mediaElement.duration,
                                'mediaSource duration increased by new append');
                  assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to reducedDuration');

                  test.done();
              });
          }, 'Test appendBuffer completes previous seek to reduced duration');

          mediasource_duration_below_currentTime_seek_test(function(test, mediaElement, mediaSource, segmentInfo,
                                                           sourceBuffer, mediaData, reducedDuration)
          {
              // The duration has been reduced at this point, and there is an
              // outstanding seek pending.
              test.expectEvent(mediaSource, 'sourceended', 'endOfStream acknowledged');

              // FIXME: Investigate if 'playing' should fire here. See http://crbug.com/266592.

              test.expectEvent(mediaElement, 'timeupdate', 'timeupdate while finishing seek to reducedDuration');
              test.expectEvent(mediaElement, 'seeked', 'seeked to reducedDuration');

              // Call endOfStream() to complete the pending seek.
              mediaSource.endOfStream();

              test.waitForExpectedEvents(function()
              {
                  assert_equals(mediaElement.currentTime, reducedDuration,
                                'Playback time has reached reducedDuration');
                  assert_equals(mediaElement.duration, reducedDuration,
                                'mediaElement duration matches reducedDuration after seek to it');
                  assert_equals(mediaSource.duration, reducedDuration,
                                'mediaSource duration matches reducedDuration after seek to it');
                  assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to reducedDuration');

                  test.done();
              });
          }, 'Test endOfStream completes previous seek to reduced duration');

          mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
          {
              assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration');

              var fullDuration = segmentInfo.duration;
              var twiceFullDuration = fullDuration * 2;
              var expectedDuration = fullDuration;

              var durationchangeEventCounter = 0;
              var durationchangeEventHandler = test.step_func(function(event)
              {
                  assert_equals(mediaElement.duration, expectedDuration, 'mediaElement duration matches expectedDuration');
                  assert_equals(mediaSource.duration, expectedDuration, 'mediaSource duration matches expectedDuration');
                  durationchangeEventCounter++;
              });

              mediaElement.addEventListener('durationchange', durationchangeEventHandler);

              // Append all the segments
              test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer append completed');
              test.expectEvent(mediaElement, 'durationchange', 'mediaElement durationchange as part of reaching loadedmetadata');
              test.expectEvent(mediaElement, 'loadedmetadata', 'mediaElement loadedmetadata');
              sourceBuffer.appendBuffer(mediaData);

              test.waitForExpectedEvents(function()
              {
                  assert_equals(mediaElement.duration, fullDuration, 'mediaElement duration matches fullDuration');
                  assert_equals(mediaSource.duration, fullDuration, 'mediaSource duration matches fullDuration');
                  assert_equals(mediaElement.currentTime, 0, 'mediaElement currentTime check - we\'re not playing');

                  // The first append's init segment triggers an initial 'durationchange' event, and
                  // the expectEvent for durationchange, above, should ensure this assertion passes.
                  // Note that the init segment in mediaData should have a duration value as large
                  // as or larger than the duration of media in mediaData, so that append should
                  // trigger precisely 1 'durationchange' event.
                  assert_equals(durationchangeEventCounter, 1, 'Should have exactly 1 "durationchange" event processed here');

                  // Increase duration. This should result in a second 'durationchange' fired.
                  expectedDuration = twiceFullDuration;
                  mediaSource.duration = twiceFullDuration;
                  assert_false(sourceBuffer.updating, 'sourceBuffer.updating after duration set to twiceFullDuration');
                  assert_equals(mediaElement.duration, twiceFullDuration, 'mediaElement duration matches twiceFullDuration');

                  // Set duration again. Later, we verify this doesn't trigger another
                  // 'durationchange' event.
                  mediaSource.duration = twiceFullDuration;

                  assert_false(sourceBuffer.updating, 'sourceBuffer.updating after duration set again to twiceFullDuration');
                  assert_equals(mediaElement.duration, twiceFullDuration, 'mediaElement duration matches twiceFullDuration after mediaSource duration set again to twiceFullDuration');

                  // Use a seek to let any currently queued 'durationchange' events fire.
                  assert_false(mediaElement.seeking, 'mediaElement should not be seeking after setting twiceFullDuration twice');
                  assert_equals(mediaElement.currentTime, 0, 'mediaElement currentTime before seeking after setting twiceFullDuration twice');
                  test.expectEvent(mediaElement, 'seeked', 'mediaElement seeked after setting twiceFullDuration twice');
                  mediaElement.currentTime = fullDuration / 2;
              });

              test.waitForExpectedEvents(function()
              {
                  assert_equals(durationchangeEventCounter, 2, 'durationchange count check before endOfStream()');

                  // Mark endOfStream to trigger duration reduction.
                  test.expectEvent(mediaSource, 'sourceended', 'endOfStream acknowledged');
                  expectedDuration = fullDuration;
                  mediaSource.endOfStream();

                  // endOfStream should reduce the duration back to fullDuration (and queue another 'durationchange').
                  assert_equals(mediaElement.duration, fullDuration, 'mediaElement duration returns to fullDuration after endOfStream()');
                  assert_equals(mediaSource.duration, fullDuration, 'mediaSource duration returns to fullDuration after endOfStream()');

                  // Use another seek to a distinct time to let any currently queued
                  // 'durationchange' events fire.
                  assert_false(mediaElement.seeking, 'mediaElement should not be seeking after calling endOfStream()');
                  test.expectEvent(mediaElement, 'seeked', 'mediaElement completed final seek');
                  mediaElement.currentTime -= fullDuration / 4;
              });

              test.waitForExpectedEvents(function()
              {
                  mediaElement.removeEventListener('durationchange', durationchangeEventHandler);
                  // Counter should be 3 because each of the following should have triggered
                  // durationchange:
                  //   1) initial appendBuffer()'s causing transition to loadedmetadata
                  //   2) explicitly increasing mediaSource.duration (to same value twice, so just
                  //      one corresponding durationchange should have been triggered).
                  //   3) endOfStream() reducing the duration
                  assert_equals(durationchangeEventCounter, 3, 'final durationchange count check');
                  test.done();
              });
          }, 'Test setting same duration multiple times does not fire duplicate durationchange');

          mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
          {
              assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration');

              var fullDuration = segmentInfo.duration;

              // Append all the segments
              test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer');

              // TODO(wolenetz): Remove this flakiness workaround. See https://crbug.com/641121#c2.
              test.expectEvent(mediaElement, 'loadedmetadata', 'mediaElement');

              sourceBuffer.appendBuffer(mediaData);

              test.waitForExpectedEvents(function()
              {
                  assert_equals(mediaElement.duration, fullDuration, 'mediaElement duration matches fullDuration');
                  assert_equals(mediaSource.duration, fullDuration, 'mediaSource duration matches fullDuration');

                  // TODO(wolenetz): Fine-tune this test to use the buffered attribute's highest end time
                  // instead of fullDuration once Chrome correctly reports buffered PTS, not DTS.
                  // See https://crbug.com/398130.

                  // Setting duration to same as current, or increasing it, should not trigger exception.
                  mediaSource.duration = fullDuration;
                  mediaSource.duration = fullDuration + 1;

                  // Reducing duration to below the highest buffered PTS should trigger exception.
                  assert_throws_dom('InvalidStateError',
                      function() { mediaSource.duration = fullDuration - 0.05; },
                      'Duration reduction that truncates at least one whole coded frame throws an exception.');

                  assert_equals(mediaSource.duration, fullDuration + 1, 'mediaSource duration matches fullDuration+1');

                  // Reducing duration without truncating any buffered media should not trigger exception.
                  mediaSource.duration = fullDuration;

                  // Reducing duration by less than the minimum of the last test audio and video frame
                  // durations should not trigger exception.
                  mediaSource.duration = fullDuration - 0.001;
                  test.done();
              });
          }, 'Test duration reduction below highest buffered presentation time is disallowed');

        </script>
    </body>
</html>
